/**
 *  工具库
 *  @author 蛋蛋 <i@hzh.li>
 *  @createtime 2021.12.20
 */
import dayjs from 'dayjs'
import { hasProp, isObject, isArray, isFunction, isPromise, isBase64 } from './validate'

/**
 *  暂歇
 *  @param  {Number}  [second]  等待时间 (单位: 秒)
 *  @return {Promise}
 */
export function sleep(second = 0) {
  return new Promise(resolve => setTimeout(resolve, second * 1000))
}

/**
 *  异步化API
 *  @param  {Function}        fn        执行函数
 *  @param  {Object}          [options] 调用传参
 *  @param  {Object[String]}  [keys]    回调键名 { success = 'resolve', fail = 'reject', ...[]: ( 'resolve' | 'reject' | Function ) }
 *  @return {Promise}
 */
export function promisify(fn, options, keys) {
  return new Promise((resolve, reject) => {
    if (!isFunction(fn)) return reject(new TypeError('"fn" is not a function'))
    let completed = false, newFn = (fn) => (function (e) { completed || (completed = true, fn(e)) })
    options ||= {}, keys = { success: 'resolve', fail: 'reject', ...keys }
    resolve = newFn(resolve), reject = newFn(reject)
    for (let prop in keys) {
      let item = keys[prop]
      item === 'resolve' ? (options[prop] = resolve) : item === 'reject' ? (options[prop] = reject) :
      isFunction(item) ? (options[prop] = newFn(item)) : prop in options && delete options[prop]
    }
    fn(options)
  })
}

/**
 *  时间范围化
 *  @param  {String|Number|Date}  startDate   开始日期 ( String: 日期文本 | Number: 时间戳 | Date: 日期对象 )
 *  @param  {String|Number|Date}  endDate     结束日期 ( String: 日期文本 | Number: 时间戳 | Date: 日期对象 )
 *  @param  {Number}              [precision] 输出精度 ( 1: 年 | 2: 年月 | 3: 默认值，年月日 | 4: 年月日时 | 5: 年月日时分 | 6: 年月日时分秒 )
 *  @param  {String}              [lang]      语言类型 ( 1: 默认值，符号 | 2: 汉字 )
 *  @return {String}
 */
export function scopeTime(startDate, endDate, precision = 3, lang = 1) {
  startDate = dayjs(startDate), endDate = dayjs(endDate)
  if (!startDate.isValid() || !endDate.isValid()) return ''
  let node = 0, prec = precision >= 1 && precision <= 6 ? parseInt(precision) : precision > 6 ? 6 : 3
  let unit = lang == 2
    ? ['YYYY年', 'M月', 'D日', prec == 4 ? 'H点' : 'HH', ':mm', ':ss']
    : ['YYYY', '-MM', '-DD', prec == 4 ? ' HH:mm' : ' HH', ':mm', ':ss']
  let same = [
    startDate.year() === endDate.year(),
    startDate.month() === endDate.month(),
    startDate.date() === endDate.date(),
    startDate.hour() === endDate.hour(),
    startDate.minute() === endDate.minute(),
    startDate.second() === endDate.second()
  ]
  while (node < prec && same[node] && ++node) {}
  let startFormat = unit.slice(0, prec).join('')
  let endFormat = unit.slice(node == 4 ? 3 : node, prec).join('').replace(/^[\-\:\s]+/, '')
  let startValue = startFormat ? startDate.format(startFormat) : ''
  let endValue = endFormat ? endDate.format(endFormat) : ''
  return `${startValue}${!endValue ? '' : lang == 2 ? ' 至 ' : ' ～ '}${endValue}`
}

/**
 *  时长格式化
 *  @param  {Number}                  time      时间长度
 *  @param  {plugin.DurationUnitType} [unit]    时长单位 (枚举参考 dayjs -> plugin.DurationUnitType)
 *  @param  {String}                  [format]  转换格式 ( 'data': 默认值，年月日时分秒的对象 | 'data-as': 以年月日时分秒分别为单位换算的对象 | ['auto', 'auto-en]: UTC格式自动化 | 'auto-cn': UTC汉字格式自动化 )
 *  @return {String|Object}
 */
export function durationTime(time, unit = 'ms', format = 'data') {
  let duration = dayjs.duration(time, unit)
  if (!format || format == 'data' || format == 'data-as') {
    let units = ['years', 'months', 'weeks', 'days', 'hours', 'minutes', 'seconds', 'milliseconds']
    let asUnits = ['asYears', 'asMonths', 'asWeeks', 'asDays', 'asHours', 'asMinutes', 'asSeconds', 'asMilliseconds']
    return units.reduce((o, k, i) => (o[k] = duration[format == 'data-as' ? asUnits[i] : k](), o), {})
  } else if (format == 'auto' || format == 'auto-en' || format == 'auto-cn') {
    let ymd = format == 'auto-cn'
      ? duration.years() && 'Y年M月D日' || duration.months() && 'M月D日' || duration.days() && 'D日' || ''
      : duration.years() && `${duration.years() > 9 ? 'Y' : 'YY'}-MM-DD` || duration.months() && 'MM-DD' || duration.days() && 'DD' || ''
    let hms = format == 'auto-cn'
      ? (ymd || duration.hours()) && 'H时m分s秒' || duration.minutes() && 'm分s秒' || 's秒'
      : (ymd || duration.hours()) && 'HH:mm:ss' || duration.minutes() && 'mm:ss' || 's[s]'
    return duration.format(`${ymd}${format != 'auto-cn' && ymd ? ' ' : ''}${hms}`)
  } else {
    return duration.format(format)
  }
}

/**
 *  生成日期范围列表
 *  @param {String}   startDate         开始日期
 *  @param {String}   endDate           结束日期
 *  @param {Object}   [options]         配置项
 *  @param {String}   [options.format]  日期格式，默认: 'YYYY年MM月DD日 HH:mm:ss'
 *  @param {String}   [options.unit]    日期单位，默认: 'day'，可选: 'year' | 'month' | 'week'或'isoWeek' | 'day' | 'hour' | 'minute' | 'second'
 *  @param {Number}   [options.step]    递增步长，默认: 1
 *  @param {Boolean}  [options.fix]     头尾日期与开始/结束日期不一致时是否补充准确的值，默认: false
 *  @return {Array<String>}
 */
export function generateDateList(startDate, endDate, options) {
  let { format = 'YYYY-MM-DD HH:mm:ss', unit = 'day', step = 1, fix = false } = options || {}
  let result = [], start = dayjs(startDate), end = dayjs(endDate)
  let diff = end.startOf(unit).diff(start.startOf(unit), unit)
  for (let i = 0; i <= diff; i += step) result.push(start.add(i, unit).format(format))
  if (fix) {
    let startValue = start.format(format)
    let endValue = end.format(format)
    if (result[0] !== startValue) result.unshift(startValue)
    if (result[result.length - 1] !== endValue) result.push(endValue)
  }
  return result
}

/**
 *  从指定日期的范围内生成日期范围列表
 *  @param {String}   [date]            日期源，默认为当天
 *  @param {Object}   [options]         配置项
 *  @param {String}   [options.minDate] 最小开始日期
 *  @param {String}   [options.maxDate] 最大结束日期
 *  @param {String}   [options.format]  日期格式，默认: 'YYYY年MM月DD日 HH:mm:ss'
 *  @param {String}   [options.type]    范围类型，默认值: 'isoWeek'，可选: 'year' | 'month' | 'week'或'isoWeek' | 'hour' | 'minute' | 'second'
 *  @param {String}   [options.unit]    日期单位，默认: 'day'，可选: 'year' | 'month' | 'week'或'isoWeek' | 'day' | 'hour' | 'minute' | 'second'
 *  @param {Number}   [options.step]    递增步长，默认: 1
 *  @param {Boolean}  [options.fix]     头尾日期与开始/结束日期不一致时是否补充准确的值，默认: false
 *  @return {Array<String>}
 */
export function generateDateListByDate(date, options) {
  let { minDate, maxDate, type = 'isoWeek', unit = 'day', ...cfg } = options || {}
  let specifyDate = date != null ? dayjs(date) : dayjs()
  let startDate = specifyDate.startOf(type)
  let endDate = specifyDate.endOf(type)
  if (minDate != null && startDate.isBefore(dayjs(minDate), unit)) startDate = dayjs(minDate)
  if (maxDate != null && endDate.isAfter(dayjs(maxDate), unit)) endDate = dayjs(maxDate)
  return generateDateList(startDate, endDate, { unit, ...cfg })
}

/**
 *  generateUUID 生成UUID
 *  @param  {String}  prefix  前缀
 *  @return {String}          返回字符串 例如：7ceb31a7-41b9-45ed-915b-14c7ad0fddf6
 */
export function generateUUID(prefix = 'u') {
  let d = new Date().valueOf()
  return `${prefix}xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx`.replace(/[xy]/g, c => {
    let r = (d + Math.random() * 16) % 16 | 0
    d = Math.floor(d / 16)
    return (c === 'x' ? r : (r & 0x7 | 0x8)).toString(16)
  })
}

/**
 *  格式化数字
 *  @description  支持高精度格式化，可去除科学计数法，精度保留至无限
 *  @param  {*}       value                   输入源
 *  @param  {Object}  [options]               配置项
 *  @param  {Number}  [options.min]           最小值
 *  @param  {Number}  [options.max]           最大值
 *  @param  {Number}  [options.precision]     数字精度 (结果保留的浮点位数)
 *  @param  {Number}  [options.sumPrecision]  算术精度 (计算结果的实际精度)
 *  @param  {*}       [options.defaultValue]  数字无效时的默认值
 *  @param  {Boolean} [options.defaultMinMax] 默认值是否使用最小值或最大值 ( true: 默认值，存在最小或最大值时默认使用 | false: 不使用 )
 *  @param  {Number}  [options.zeroFill]      是否补零 ( 默认不补零 | Number: 自定义长度补零 )
 *  @param  {Boolean} [options.milFill]       是否补充千分位符号 ( 默认值不使用 | true: 使用 )
 *  @return {String}
 */
export function formatNumber(value, options) {
  let { min, max, precision, sumPrecision, defaultMinMax = true, zeroFill, milFill } = options || {}
  let result = value, defaultValue = hasProp(options, 'defaultValue') ? options.defaultValue : ''
  if (!(
    (typeof result == 'number' || typeof result == 'string' && result !== '') && !isNaN(result) ||
    typeof result == 'bigint' && (result = String(result))
  )) return defaultValue
  if (min != null && result < +min) {
    if (!defaultMinMax) return defaultValue; result = min
  } else if (max != null && result > +max) {
    if (!defaultMinMax) return defaultValue; result = max
  }
  if (!isFinite(result)) return defaultValue
  if (/0(b|o|x)/i.test(result)) result = String((typeof BigInt == 'function' ? BigInt : Number)(result))
  if (sumPrecision != null && (precision == null || precision > sumPrecision)) result = numToFixed(result, sumPrecision)
  result = numToFixed(result, precision)
  if (zeroFill > 0 || milFill) {
    let { 1: minus = '', 2: int = '', 3: any = '' } = /^(-?)(\d+)(.*)/.exec(result) || {}
    if (zeroFill > int.length) int = `${''.padEnd(zeroFill - int.length, 0)}${int}`
    if (milFill) {
      let r = '', s = 3, i = int.length
      while ((i -= s) > -s) r = `${int.slice(i > 0 ? i : 0, i + s)}${r ? ',' : ''}${r}`
      int = r
    }
    result = `${minus}${int}${any}`
  }
  return result
}

/**
 *  计算数值
 *  @param  {Array}   expression      计算表达式 (相当于括号包囊的表达式，如 [1, '+', 2, '*', 3, '-', 4] = (1 + 2 * 3 - 4)，支持的运算符: ('**', '*', '/', '%', '+', '-', '<<', '>>', '>>>', '&', '^', '|')，优先级从左往右递减)
 *  @param  {Object}  [formatOptions] 结果值格式化配置 (同 formatNumber)
 *  @return {String}
 */
export function computeNumber(expression, formatOptions) {
  if (!isArray(expression)) throw new SyntaxError('入参表达式应为数组')
  let symbols = ['**', '*', '/', '%', '+', '-', '<<', '>>', '>>>', '&', '^', '|']
  let compute = (exp) => {
    exp = exp.map(v => isArray(v) ? compute(v) : v)
    if (exp.length == 1) return +exp[0] || 0
    if (exp.length % 2 != 1 || exp.some((v, i) => i % 2 == 1 && !symbols.includes(v))) throw new SyntaxError('计算表达式有误')
    let syms = symbols.filter(v => exp.includes(v))
    while (syms.length) {
      let idx = exp.indexOf(syms[0])
      if (idx == -1) { syms.shift(); continue }
      let v, a = +exp[idx - 1] || 0, b = +exp[idx + 1] || 0
      switch (exp[idx]) {
        case '**': v = a ** b; break
        case '*': v = a * b; break
        case '/': v = a / b; break
        case '%': v = a % b; break
        case '+': v = a + b; break
        case '-': v = a - b; break
        case '<<': v = a << b; break
        case '>>': v = a >> b; break
        case '>>>': v = a >>> b; break
        case '&': v = a & b; break
        case '^': v = a ^ b; break
        case '|': v = a | b; break
      }
      exp.splice(idx - 1, 3, v)
    }
    return +exp[0] || 0
  }
  return formatNumber(compute(expression), formatOptions)
}

/**
 *  数组排序
 *  @param  {Array}                 data  源数据
 *  @param  {String|Array|Function} path  排序字段键名 ( [String, Array[String]]: 按路径获取排序值 | Function: 自定义获取排序值，参数: (data: 当前对象) )
 *  @param  {String|Array}          order 排序顺序，按参数顺序过滤和排序。默认值: ['number', 'en', 'cn', 'symbol', '*', 'empty']；预设参数: ( '*': 过滤剩余的值 | 'null': 为null的值 | 'empty': 为空的值 | 'text': 有效字符 | 'number': 数字 | 'symbol': 基础特殊字符 | 'url': 链接 | ['chinese', 'cn']: 汉字 | ['english', 'en']: 大小写英文 | ['english-upper', 'en-up']: 大写英文 | ['english-lower', 'en-low']: 小写英文 )；若参数为对象时可自定义排序 { filter, sort }；若参数为正则时按正则过滤。
 *  @return {Array}                       排序后的新数组
 */
export var sorter = (function () {
  const { get } = require('lodash')
  const regs = {
    number: /^-?(\d+|\d+\.\d*|\d*\.\d+)$/,
    symbol: /^(?=[\x21-\x7e]+)[^A-Za-z0-9]/,
    chinese: /^[\u4e00-\u9fa5]/,
    english: /^[A-Za-z]/,
    englishUpper: /^[A-Z]/,
    englishLower: /^[a-z]/,
    url: /^(https?|ftps?):\/\/.+/
  }
  const presets = {
    'null': {
      filter: (v) => v == null
    },
    'empty': {
      filter: (v) => v == null || v === ''
    },
    'text': {
      filter: (v) => typeof v == 'string' && v !== '' || typeof v == 'number' && !isNaN(v),
      sort: (a, b) => String(a).localeCompare(b)
    },
    'number': {
      filter: (v) => !presets.empty.filter(v) && regs.number.test(v),
      sort: (a, b) => a - b
    },
    'symbol': {
      filter: (v) => !presets.empty.filter(v) && regs.symbol.test(v)
    },
    'url': {
      filter: (v) => !presets.empty.filter(v) && regs.url.test(v)
    },
    'chinese': {
      filter: (v) => !presets.empty.filter(v) && regs.chinese.test(v),
      sort: (a, b) => String(a).localeCompare(b)
    },
    'english': {
      filter: (v) => !presets.empty.filter(v) && regs.english.test(v),
      sort: (a, b) => a > b ? 1 : a < b ? -1 : 0
    },
    'english-upper': {
      filter: (v) => !presets.empty.filter(v) && regs.englishUpper.test(v),
      sort: (a, b) => a > b ? 1 : a < b ? -1 : 0
    },
    'english-lower': {
      filter: (v) => !presets.empty.filter(v) && regs.englishLower.test(v),
      sort: (a, b) => a > b ? 1 : a < b ? -1 : 0
    },
    get 'cn'() { return presets['chinese'] },
    get 'en'() { return presets['english'] },
    get 'en-up'() { return presets['english-upper'] },
    get 'en-low'() { return presets['english-lower'] }
  }
  return function (data, path, order) {
    if (!isArray(data)) return []
    if (!isArray(order)) order = typeof order == 'string' ? order.split(',') : ['number', 'en', 'cn', 'symbol', '*', 'empty']
    let result = [], group = [], list = data.concat(), remain = -1, pathType = path == null ? 0 : isFunction(path) ? 1 : 2
    for (let index = 0, length = order.length, record = []; index < length; index++) {
      let item = order[index], itemList = []
      if (!item || record.includes(item) && (record.push(item), true)) continue
      if (item === '*') { group.push(item), remain = index; continue }
      let { filter, sort } =
        typeof item == 'string' && presets[item] ||
        isFunction(item.filter) && item ||
        item instanceof RegExp && { filter: (v) => !presets.empty.filter(v) && item.test(v) } || {}
      if (!filter) continue
      for (let i = 0, j = 0, l = list.length, g = list.concat(); i < l; i++) {
        let v = pathType == 0 ? g[i] : pathType == 1 ? path(g[i]) : get(g[i], path)
        if (filter(v, i, g)) itemList.push(g[i]), list.splice(i - j, 1), j++
      }
      if (sort !== null && sort !== false) {
        let isSortFN = isFunction(sort)
        itemList.sort(pathType == 0 ? (isSortFN ? sort : void 0) : function (a, b) {
          a = pathType == 1 ? path(a) : get(a, path)
          b = pathType == 1 ? path(b) : get(b, path)
          return isSortFN ? sort(a, b) : a > b ? 1 : a < b ? -1 : 0
        })
      }
      group.push(itemList)
    }
    if (remain != -1) group[remain] = list.sort()
    for (let i = 0, l = group.length; i < l; i++) group[i].length && result.push(...group[i])
    if (remain == -1 && list.length) result.push(...list.sort())
    return result
  }
})()

/**
 *  数组转字符串 (值转JSON字符串，过滤空值)
 *  @param  {Array}   source  源数组
 *  @param  {String}  symbol  分隔符 (默认值 ',')
 *  @return {String}
 */
export function arrToStr(source, symbol) {
  if (typeof source == 'string') return source
  if (!isArray(source)) return ''
  let result = []
  for (let item of source) {
    if (item == null || item === '') continue
    if (typeof item == 'string' || typeof item == 'number') {
      result.push(item)
    } else {
      try {
        let jsonItem = JSON.stringify(item)
        if (jsonItem != null && jsonItem !== '') result.push(jsonItem)
      } catch (e) {}
    }
  }
  return result.join(symbol != null ? symbol : ',')
}

/**
 *  字符串转数组 (过滤空值)
 *  @param  {String}  source  源字符串
 *  @param  {String}  symbol  分隔符 (默认值 ',')
 *  @return {Array}
 */
export function strToArr(source, symbol) {
  if (isArray(source)) return source.filter(v => v != null && v !== '')
  if (typeof source != 'string' || !source) return []
  let result = []
  for (let item of source.split(symbol != null ? symbol : ',')) {
    item = item.trim(), item && result.push(item)
  }
  return result
}

/**
 *  字符串转驼峰风格
 *  @param  {String}  source      源字符串
 *  @param  {String}  symbol      转换符 (默认值 '-')
 *  @param  {Boolean} upperFirst  是否将首位字符转为大写
 *  @return {String}
 */
export function strToHump(source, symbol, upperFirst) {
  if (typeof source != 'string' || !source) return ''
  if (symbol != null) symbol = String(symbol).replace(/\\/g, '\\\\')
  let result = source.replace(RegExp(`(${symbol || '-'})+([A-Za-z])`, 'g'), (p0, p1, p2) => p2.toUpperCase())
  if (upperFirst == true) result = result.substring(0, 1).toUpperCase() + result.substring(1)
  return result
}

/**
 *  设置掩码
 *  @param  {String}  value   输入源
 *  @param  {Number}  start   起始位置 (默认值: 3，为负数时按倒序确定位置)
 *  @param  {Number}  end     结束位置 (默认值: -3，为负数时按倒序确定位置)
 *  @param  {String}  symbol  掩码字符 (默认值: '*')
 *  @return {String}
 */
export function strMask(value, start = 3, end = -3, symbol = '*') {
  return value == null ? '' : String(value).split('').fill(symbol, start, end).join('')
}

/**
 *  为数值添加分隔符
 *  @param  {Number|String} value     含有数值的文本
 *  @param  {Number}        scale     分隔刻度 (默认值: 3)
 *  @param  {String}        separator 分隔符号 (默认值: ',')
 *  @return {String}                  文本中所有整数值均转换
 */
export function numBreak(value, scale = 3, separator = ',') {
  let reg = /(\d+)(\.\d+)?/g
  return value == null ? '' : reg.test(value) ? String(value).replace(reg, (_, a = '', b = '') => {
    let r = '', l = a.length, i = l, n = 0
    while (n++, --i >= 0) r = `${n % scale == 0 && n < l ? separator : ''}${a[i]}${r}`
    return r + b
  }) : value
}

/**
 *  高精度小数取舍
 *  @param  {Number}  value       输入源
 *  @param  {Number}  [precision] 数字精度 (可保留的浮点位数，为空时仅精度化不进行取舍)
 *  @return {String}
 */
export function numToFixed(value, precision) {
  if (!isFinite(value)) return String(+value)
  let reg = /^(-?)0*(\d*)(\.?)(\d*?)0*(e(-|\+)?(\d+))?$/i
  let { 1: minus = '', 2: int = '', 4: dec = '', 6: dir, 7: fe } = reg.exec(value) || {}
  if (minus && !(int || dec)) minus = ''
  if (fe > 0 && (int || dec)) {
    if (dir != '-') {
      let df = fe - dec.length
      int = df < 0 ? `${int}${dec.slice(0, df)}` : `${int}${dec}${''.padEnd(df, 0)}`
      dec = df < 0 ? dec.slice(df) : ''
    } else {
      let df = fe - int.length
      dec = df < 0 ? `${int.slice(-df)}${dec}` : `${''.padEnd(df, 0)}${int}${dec}`
      int = df < 0 ? int.slice(0, -df) : ''
    }
  }
  if (precision != null) {
    precision = Math.max(Math.floor(precision), 0) || 0
    if (precision >= dec.length) {
      dec = dec.padEnd(precision, 0)
    } else {
      let grd = dec[precision] > 4; dec = dec.slice(0, precision)
      if (grd) {
        let r = '', b = `${int || 0}${dec}`, d = dec.length, i = b.length
        while (--i >= 0) {
          if (b[i] < 9) {
            r = `${b.slice(0, i)}${+b[i] + 1}${r}`; break
          } else {
            r = `${i == 0 ? '1' : ''}0${r}`
          }
        }
        int = d ? r.slice(0, -d) : r
        dec = d ? r.slice(-d) : ''
      }
    }
  }
  return `${minus}${int || 0}${dec ? `.${dec}` : ''}`
}

/**
 *  数字转Excel字母列
 *  @param  {Number}  n 列位 (从0开始)
 *  @return {String}    列名 (A-Z, AA-ZZ, ...)
 */
export function numToCellPos(n) {
  let r = ''
  while (n >= 0) {
    r = String.fromCharCode(n % 26 + 65) + r
    n = Math.floor(n / 26) - 1
  }
  return r
}

/**
 *  格式化路径
 *  @param  {String|Array}  source  源路径 (为数组时以"/"分隔)
 *  @return {String}                格式化为 `xxx/xxx` || `/xxx/xxx` || `xxx://xxx/xxx` 的路径
 */
export function formatPath(source) {
  if (isArray(source)) source = source.join('/')
  if (typeof source != 'string' || !source) return ''
  if (isBase64(source)) return source
  return source.replace(/(^|[^:])\/+/g, '$1/').replace(/(.+?)\/+$/, '$1')
}

/**
 *  对象转URI参数
 *  @param  {Object}  source  URI参数源对象
 *  @return {String}
 */
export function objToUri(source) {
  let result = ''
  if (!isObject(source)) return result
  for (let prop in source) {
    let value = source[prop]
    if (isObject(value)) {
      try { value = JSON.stringify(value) } catch (e) { continue }
    }
    if (value !== void 0) {
      result += `${result ? '&' : ''}${encodeURIComponent(prop)}=${encodeURIComponent(value)}`
    }
  }
  return result
}

/**
 *  URI参数转对象
 *  @param  {String}  source  转换为对象的URI参数
 *  @return {Object}
 */
export function uriToObj(source) {
  let result = {}
  let { 1: search } = typeof source == 'string' && /^.*?\?([^#]+)/.exec(source) || {}
  if (!search) return result
  let concat = Array.prototype.concat.bind([])
  for (let param of search.split('&')) {
    let paramExec = /^(.*?)=(.*)/.exec(param)
    if (!paramExec) continue
    let key = decodeURIComponent(paramExec[1])
    let value = decodeURIComponent(paramExec[2])
    try {
      let json = JSON.parse(value), type = typeof json
      if (type == 'boolean' || type == 'object' && json) value = JSON.parse(value)
    } catch (e) {}
    if (result[key] == null || result[key] === '') {
      result[key] = value
    } else if (value != null && value !== '') {
      result[key] = concat(result[key], value)
    }
  }
  return result
}

/**
 *  解析URL链接信息
 *  @param  {String}  source  解析的URL链接
 *  @return {Object}
 */
export function parseUrl(source) {
  let parsed = false, urls = {}, exec = typeof source == 'string' && /^((.+?):\/*)?(.*)/.exec(source) || {}
  try { exec[3] && (urls = new URL('http://' + exec[3]), parsed = true) } catch (e) {}
  let protocols = exec[2] ? exec[2].split('+').filter(Boolean) : []
  return {
    parsed, protocols, protocol: protocols[0] || '',
    port: urls.port == '0' ? 0 : +urls.port || null,
    href: `${exec[1] || ''}${(urls.href || '').slice(7)}`,
    host: urls.host || '',
    hostname: urls.hostname || '',
    username: urls.username || '',
    password: urls.password || '',
    origin: `${exec[1] || ''}${urls.host || ''}`,
    pathname: urls.pathname || '',
    hash: (urls.hash || '').slice(1),
    search: (urls.search || '').slice(1),
    query: uriToObj(urls.search)
  }
}

/**
 *  按变量解析字符串
 *  @param  {String}  str   源文本 ( 变量格式: ${...} )
 *  @param  {Object}  vars  可解析的变量
 *  @return {String}
 */
export function parseStrByVars(str, vars) {
  if (!(vars && str && typeof str == 'string')) return str
  return str.replace(/\${(.+?)}/g, ($, $1) => hasProp(vars, $1) ? vars[$1] : $)
}

/**
 *  转换为正则表达式源码
 *  @param  {String|RegExp} source  转换为正则表达式源码的源值
 *  @return {String}
 */
export function toRegSource(source) {
  return source == null || source === '' ? '' : source instanceof RegExp ? source.source : String(source).replace(/\\?\//g, '\\/')
}

/**
 *  定义默认值
 *  @param  {*} source  输入源
 *  @param  {*} targets 默认源
 *  @return {*}         处理后的输入源
 */
export function defineDefault(source, ...targets) {
  if (source == null) source = {}
  for (let target of targets) {
    if (target == null) continue
    for (let prop in target) {
      if (hasProp(target, prop) && source[prop] === void 0) {
        source[prop] = target[prop]
      }
    }
  }
  return source
}

/**
 *  连接值
 *  @param  {...*}    values  连接的值 (排除空值，值为数组时解构成同一个组)
 *  @return {*|Array}         输出的值大于一个时返回数组，否则只返回第一个
 */
export function concatValue(...values) {
  let result = []
  for (let value of values) {
    if (value == null) continue
    isArray(value) ? result.push(...value.filter(v => v != null)) : result.push(value)
  }
  return result.length > 1 ? result : result[0]
}

/**
 *  函数继承
 *  @param  {...Function} fns 继承的函数
 *  @return {Function}    继承的函数逐个执行，作用域和参数共用，执行完后返回最后一个函数的回调结果
 */
export function inheritFn(...fns) {
  fns = fns.filter(v => isFunction(v))
  return fns.length === 1 ? fns[0] : function () {
    let res
    for (let i = 0, l = fns.length; i < l; i++) {
      try {
        res = fns[i].apply(this, arguments)
      } catch (err) {
        res = err, console.error(err)
      }
    }
    return res
  }
}

/**
 *  去除对象中字符串参数的首尾空格
 *  @param  {Object}    source  源对象
 *  @param  {Boolean}   deep    是否递归处理
 *  @param  {Function}  handler 自定义处理操作，参数：(value: 当前值, prop: 当前键, source: 当前引用对象)
 *  @return                     处理后的源对象
 */
export function trimData(source, deep, handler) {
  if (isObject(source)) {
    if (!isFunction(handler)) {
      handler = function (value, prop, source) {
        if (typeof value == 'string') source[prop] = value.trim()
      }
    }
    for (let prop in source) {
      handler(source[prop], prop, source)
      if (deep) trimData(source[prop], deep, handler)
    }
  }
  return source
}

/**
 *  移除对象指定的值
 *  @param  {*}               value       处理对象
 *  @param  {Function|Array}  [excluder]  移除的值 (默认值为空值)
 *  @param  {Function}        [deleter]   
 *  @return {*}
 */
export function removeExcludes(value, excluder, deleter) {
  if (!excluder) excluder = [void 0, null, NaN, '']
  let excluderType = isFunction(excluder) ? 1 : isArray(excluder) ? 2 : 0
  if (excluderType === 0) return value
  if (isArray(value)) {
    for (let i = 0; i < value.length; i++) {
      if (excluderType === 1 ? excluder(value[i], i, value) : excluder.includes(value[i])) value.splice(i--, 1)
    }
  } else if (isObject(value)) {
    if (!isFunction(deleter)) deleter = void 0
    for (let prop in value) {
      if (excluderType === 1 ? excluder(value[prop], prop, value) : excluder.includes(value[prop])) {
        deleter ? deleter(prop, value) : delete value[prop]
      }
    }
  }
  return value
}

/**
 *  JSON.stringify (安全模式，兼容循环引用对象)
 *  @param  {*}               source        输入源
 *  @param  {Function|Array}  replacer      序列化方式 ( Function: 自定义转换 | Array: 仅转换存在的 key 值 )
 *  @param  {Number|String}   space         指定缩进用的空白字符串 ( Number: 缩进空格数 | String: 缩进符 )
 *  @param  {Function}        cycleReplacer 循环引用值的序列化方式，暴露参数: (key, value)
 *  @return {String}
 */
export function JsonStringify(source, replacer, space, cycleReplacer) {
  let stack = []
  return JSON.stringify(source, function (key, value) {
    if (stack.length > 0) {
      let thisPos = stack.indexOf(this)
      thisPos != -1 ? stack.splice(thisPos + 1) : stack.push(this)
      if (stack.indexOf(value) != -1) value = cycleReplacer ? cycleReplacer.call(this, key, value) : void 0
    } else stack.push(value)
    return replacer == null ? value : replacer.call(this, key, value)
  }, space)
}

/**
 *  连接文本
 *  @param  {Array[String]} texts               连接的文本
 *  @param  {Object}        [options]           配置项
 *  @param  {Boolean}       [options.trim]      是否清除文本首尾空格 ( true: 默认值，清除 | false: 不清除 )
 *  @param  {Boolean}       [options.intersect] 是否交集模式 ( true: 默认值，清除交集文本 | false: 仅末尾处文本与遍历文本不全等时清除 )
 *  @return {String}
 */
export function joinText(texts, options) {
  if (!isArray(texts)) return ''
  let res = '', { trim = true, intersect = true } = options || {}
  for (let item of texts) {
    if (typeof item != 'string') continue
    if (trim) item = item.trim()
    if (!item) continue
    let last = res.slice(-item.length)
    if (last === item) continue
    if (intersect) {
      let sliceIndex = 0, checkIndex = 0,
        itemLength = item.length, lastLength = last.length,
        maxLength = itemLength > lastLength ? lastLength : itemLength
      while (++checkIndex <= maxLength) {
        if (item.slice(0, checkIndex) === last.slice(-checkIndex)) sliceIndex = checkIndex
      }
      res += sliceIndex > 0 ? item.slice(sliceIndex) : item
    } else {
      res += item
    }
  }
  return res
}

/**
 *  复制文本到剪切板
 *  @param  {String|Number} value 需要复制的文本
 *  @return {Boolean}
 */
export function copyText(value) {
  if (value == null || value === '') return false
  let textarea = document.createElement('textarea')
  textarea.value = value
  document.body.appendChild(textarea)
  textarea.focus()
  textarea.setSelectionRange ? textarea.setSelectionRange(0, textarea.value.length) : textarea.select()
  let result = document.execCommand('copy')
  document.body.removeChild(textarea)
  return result
}

/**
 *  创建防抖/节流的函数
 *  @param  {Function}  func          目标函数
 *  @param  {Object}    options       配置项
 *  @param  {Number}    options.wait  延迟时间 (单位: 毫秒，为 0 时同步执行)
 *  @param  {Number}    options.mode  处理模式 ( 'debounce': 默认值，防抖 | 'throttle': 节流 )
 *  @param  {Number}    options.type  处理类型 ( 1: 默认值。防抖模式下：非立即执行，仅防抖结束后异步执行，无回调；节流模式下：时间戳版，节流过程中无阻塞时同步执行，节流结束后不再执行，可回调 | 2: 防抖模式下：立即执行，仅防抖开始时同步执行，可回调；节流模式下：定时器版，节流过程中无阻塞时异步执行，节流结束后必执行最后一次，无回调 )
 *  @return                           包装函数 (函数属性 mode, type, wait 可控配置)
 */
export function createWaitFn(func, options) {
  let context, args, timeout, now, previous
  let debounceLater = function () {
    let diff = Date.now() - now, { type, wait } = fn
    if (diff < wait && diff > 0) {
      timeout = setTimeout(debounceLater, wait - diff)
    } else {
      timeout = null, type != 2 && func.apply(context, args)
    }
  }
  let throttleLater = function () {
    timeout = null, func.apply(context, args)
  }
  let fn = function () {
    now = Date.now(), context = this, args = arguments
    let { mode, type, wait } = fn
    if (wait > 0) {
      if (mode == 'throttle') {
        if (type == 2) {
          timeout ||= setTimeout(throttleLater, wait)
        } else if (!previous || now - previous > wait) {
          previous = now
          return func.apply(context, args)
        }
      } else if (!timeout) {
        timeout = setTimeout(debounceLater, wait)
        if (type == 2) return func.apply(context, args)
      }
    } else {
      if (timeout) clearTimeout(timeout), timeout = null
      if (previous) previous = null
      return func.apply(context, args)
    }
  }
  fn.mode = options?.mode || 'debounce'
  fn.type = options?.type ?? 1
  fn.wait = options?.wait ?? 200
  return fn
}

/**
 *  函数防抖
 *  @param  {Function}  func      目标函数
 *  @param  {Number}    wait      延迟时间 (单位: 毫秒，为 0 时同步执行)
 *  @param  {Number}    immediate 是否同步执行 ( false: 默认值，非立即执行，仅防抖结束后异步执行，无回调 | true: 立即执行，仅防抖开始时同步执行，可回调 )
 *  @return {Function}            包装函数 (函数属性同 createWaitFn)
 */
export function debounce(func, wait = 200, immediate = false) {
  return createWaitFn(func, { mode: 'debounce', type: immediate ? 2 : 1, wait })
}

/**
 *  函数节流
 *  @param  {Function}  func      目标函数
 *  @param  {Number}    wait      延迟时间 (单位: 毫秒，为 0 时同步执行)
 *  @param  {Boolean}   immediate 是否同步执行 ( true: 默认值，时间戳版，节流过程中无阻塞时同步执行，节流结束后不再执行，可回调 | false: 定时器版，节流过程中无阻塞时异步执行，节流结束后必执行最后一次，无回调 )
 *  @return {Function}            包装函数 (函数属性同 createWaitFn)
 */
export function throttle(func, wait = 200, immediate = true) {
  return createWaitFn(func, { mode: 'throttle', type: immediate ? 1 : 2, wait })
}

/**
 *  创建深度/广度递归函数
 *  @param  {Function}      fn    需包装的源函数，参数：(next: 下一次递归的函数，无递归模式时为null，广度优先时设置 next.break = true 可跳出执行队列, ...args: 每次调用时传入的参数)
 *  @param  {Number|String} mode  处理模式 ( 默认无递归仅执行一次 | [1, 'dfs']: 深度优先 | [2, 'bfs']: 广度优先 )
 *  @return {Function}            返回的新函数体
 */
export function createDeep(fn, mode) {
  if (mode == 2 || mode == 'bfs') {
    return (...args) => {
      let res, queues = [args], next = (...args) => queues.push(args)
      while (queues.length && !next.break) res = fn(next, ...queues.shift())
      return res
    }
  } else if (mode == 1 || mode == 'dfs') {
    let next = (...args) => fn(next, ...args)
    return next
  } else {
    return (...args) => fn(null, ...args)
  }
}

/**
 *  创建手动操作的 Promise (可跳过或中断)
 *  @param  {Function|Promise}  executor          处理方法 ( Function | Promise: 仅pending状态可手动操作) )
 *  @param  {Object}            [options]         配置项
 *  @param  {AbortSignal}       [options.signal]  中断信号
 *  @return {Promise}                             Promise: { next: 跳过方法, abort: 中断方法 }
 */
export function createManualPromise(executor, options) {
  let manual = function (target) {
    target.then = apis[0], target.catch = apis[1], target.finally = apis[2],
    target.next = apis[3], target.abort = apis[4]
    return target
  }
  let apis = ['then', 'catch', 'finally'].map(name => function () {
    return manual(Promise.prototype[name].apply(this, arguments))
  })
  let promise = manual(Promise.race([
    isFunction(executor) ? new Promise(executor) : executor,
    new Promise((resolve, reject) => { apis[3] = resolve, apis[4] = reject })
  ]))
  let { signal } = options || {}
  if (signal) {
    let onabort = function () { promise.abort('reason' in signal ? signal.reason : new DOMException('Aborted', 'AbortError')) }
    signal.aborted ? onabort() : signal.addEventListener && signal.addEventListener('abort', onabort)
  }
  return promise
}

/**
 *  树形对象-节点查找
 *
 *  @brief  按条件查找树形对象中的节点
 *  @param  {Array}           data                树形对象源
 *  @param  {Object}          [options]           配置项
 *  @param  {String|Number}   [options.mode]      遍历模式 ( [1, 'dfs']: 默认值，深度优先 | [2, 'bfs']: 广度优先 )
 *  @param  {Function|Object} [options.filter]    过滤条件 ( Function: 回调函数判断，参数：(node: 当前节点, parentNodes: 当前节点的父节点组, results: 匹配成功的节点组) | Object[key: value]: 匹配值判断 (key: 与当前过滤节点对应的键名, value: 过滤节点对应的值，当节点对应的值与过滤条件中的值全等时收纳该节点) )
 *  @param  {String}          [options.childKey]  子节点组键名 (默认 'children')
 *  @param  {Boolean}         [options.multiple]  是否查找多个 (默认 false)
 *  @return {*|Array}                             查找结果 (查找多个时返回结果数组)
 */
export function treeFindNode(data, options) {
  let { mode, filter, childKey = 'children', multiple } = isObject(options) ? options : {}
  let results = [], result, filterFN
  if (isFunction(filter)) {
    filterFN = filter
  } else if (isObject(filter)) {
    filterFN = (node) => {
      if (!isObject(node)) return false
      for (let prop in filter) {
        if (node[prop] !== filter[prop]) return false
      }
      return true
    }
  }
  if (filterFN && isArray(data)) {
    if (mode == 2 || mode == 'bfs') {
      let queues = [], queue = { nodes: data, parentNodes: [] }
      while (queue) {
        let { nodes, parentNodes } = queue
        for (let node of nodes) {
          if (filterFN(node, parentNodes, results)) {
            if (multiple != true) return node
            results.push(node)
          }
          if (childKey && isObject(node) && isArray(node[childKey])) {
            queues.push({ nodes: node[childKey], parentNodes: parentNodes.concat([node]) })
          }
        }
        queue = queues.shift()
      }
    } else {
      let finder = (nodes, parentNodes) => {
        for (let node of nodes) {
          if (filterFN(node, parentNodes, results)) {
            if (multiple != true) { result = node; return true }
            results.push(node)
          }
          if (childKey && isObject(node) && isArray(node[childKey])) {
            if (finder(node[childKey], parentNodes.concat([node]))) break
          }
        }
      }
      finder(data, [])
    }
  }
  return multiple == true ? results : result
}

/**
 *  缓存加载
 *  @param  {Function|Promise}  method              请求方法
 *  @param  {Object}            [options]           配置项 (传参仅为1个参数且为对象时，method 作为 options, method 的值为 options.method)
 *  @param  {String}            [options.type]      缓存类型 ( 'cache': 默认值，加载最近缓存的结果 | 'final': 加载最新获取的结果 )
 *  @param  {*}                 [options.params]    请求参数 (将作为请求方法的第一个入参)
 *  @param  {Array}             [options.paramList] 请求参数列表 (存在时覆盖 params，将作为请求方法的入参集合)
 *  @param  {Object}            [options.temp]      缓存对象
 *  @param  {String}            [options.key]       缓存键名
 *  @return {Promise<any>}                          加载结果 (含 __CANCEL__ 属性时表示结果已被阻断)
 */
export async function cacheLoad(method, options) {
  if (!options && isObject(method) && !isPromise(method)) options = method, { method } = options
  let { params, paramList, type, temp, key } = options || {}
  let fn = async () => {
    if (isFunction(method)) return method.apply(null, isArray(paramList) ? paramList : 'params' in options ? [params] : [])
    if (isPromise(method)) return method
    throw new TypeError('"method" is not function or promise')
  }
  if (temp == null || key == null) return fn()
  if (type == 'final') {
    let cur = temp[key] = (+temp[key] || 0) + 1, res = await fn()
    if (cur === temp[key]) return res; else throw { __CANCEL__: true }
  } else {
    try { return await (temp[key] ||= fn()) }
    catch (e) { temp[key] = null, delete temp[key]; throw e }
  }
}

/**
 *  获取列表数据和填入操作
 *  @param  {Function|Promise}  method                请求方法 (返回 Promise<any>: { success: 验证是否成功, data: 所取列表数据 })
 *  @param  {Object}            [options]             配置项 (传参仅为1个参数且为对象时，method 作为 options, method 的值为 options.method)
 *  @param  {Array}             [options.datas]       存储数据组
 *  @param  {*}                 [options.props]       引入数据对应键名 ( true: 默认值，全部源键 | [String|Number]: 指定源键 | Array: 多个源键 | Object{key: String|Number}: { 自定义键名: 源键 } | Object{key: Function}: { 源键: 值的回调函数，返回的值作为当前键的值。参数：(source: 源键对应的值, invalidate(flag = true): 控制是否输出值的方法(flag: true时不输出，false时输出), setProp(val = 源键，替换为的键名): 嵌入键名替换的方法) } )
 *  @param  {*}                 [options.params]      请求参数 (将作为请求方法的第一个入参)
 *  @param  {Array}             [options.paramList]   请求参数列表 (存在时覆盖 params，将作为请求方法的入参集合)
 *  @param  {Boolean}           [options.safeLoad]    是否安全执行 (默认否，仅存储数据组存在时生效)
 *  @param  {Boolean}           [options.errorClean]  是否失败时清空数据组 (默认否)
 *  @param  {Function}          [options.formatter]   列表源数据处理方法。参数: (data: 成功时返回的列表源数据)，返回数组时数据组使用该值
 *  @param  {Function|false}    [options.stateChange] 请求状态变化处理事件 ( 默认 datas.loadState 存在时自动设置 | Function: 自定义处理方法，参数：(state: 当前的请求状态 { 1: 请求中 | 2: 请求成功 | 3: 请求失败 }) | false: 不设置 )
 *  @return {Promise<resolve>}                        返回 Promise<resolve>: { success: 是否成功, data: 处理后的数据列表, result: 返回的源数据 }
 */
export async function getListData(method, options) {
  if (!options && isObject(method) && !isPromise(method)) options = method, { method } = options
  let loadKey, { datas, props, params, paramList, safeLoad, errorClean, formatter, stateChange } = options ||= {}
  safeLoad = safeLoad == true && !!datas
  stateChange =
    isFunction(stateChange) ? stateChange :
    stateChange != false && datas && ('loadState' in datas) ? (state) => datas.loadState = state : null
  const setData = (list = []) => {
    if (isArray(datas)) datas.splice(0, Infinity, ...list)
  }
  const callback = (type, result, data = []) => {
    return { success: type == 1, block: type == 2, result, data }
  }
  try {
    if (stateChange) stateChange(1)
    if (safeLoad) {
      if (!('loadKey' in datas)) Object.defineProperty(datas, 'loadKey', { configurable: true, writable: true, value: 0 })
      loadKey = ++datas.loadKey
    }
    let res =
      isFunction(method) ? await method.apply(null, isArray(paramList) ? paramList : 'params' in options ? [params] : []) :
      isPromise(method) ? await method : new TypeError('"method" is not function or promise')
    if (!res || !res.success && !isArray(res)) throw res
    if (safeLoad && loadKey !== datas.loadKey) return callback(2)
    let propType = isArray(props) ? 1 : isObject(props) ? 2 : isValidText(props) ? 3 : 0
    let getData = (val) => (isArray(val) && val) || (isObject(val) && getData(val.data)) || []
    let oriData = getData(res), resData = []
    if (isFunction(formatter)) {
      let formatData = formatter(oriData)
      if (isArray(formatData)) oriData = formatData
    }
    for (let oriItem of oriData) {
      if (!isObject(oriItem)) continue
      let resItem = {}
      if (propType == 1) {
        for (let prop of props) resItem[prop] = oriItem[prop]
      } else if (propType == 2) {
        for (let prop in props) {
          if (isFunction(props[prop])) {
            let valid = true, invalidate = () => { valid = false }, setProp = (key = prop) => { prop = key }
            let itemVal = props[prop](oriItem[prop], invalidate, setProp)
            if (valid) resItem[prop] = itemVal
          } else {
            resItem[prop] = oriItem[props[prop]]
          }
        }
      } else if (propType == 3) {
        resItem[props] = oriItem[props]
      } else {
        resItem = oriItem
      }
      if (Object.keys(resItem).length) resData.push(resItem)
    }
    setData(resData)
    if (stateChange) stateChange(2)
    return callback(1, res, resData)
  } catch (err) {
    if (safeLoad && loadKey !== datas.loadKey) return callback(2)
    if (errorClean == true) setData()
    if (stateChange) stateChange(3)
    return callback(0, err)
  }
}

/**
 *  计算类名合集
 *  @param  {String|Array|Object} value 类名源 ( String | Array[String] | Object: { className: Boolean } )
 *  @return {Array}                     处理后的类名合集
 */
export function computeClass(value) {
  const res = []
  for (let val of [value].flat(Infinity)) {
    if (val == null) continue
    if (typeof val == 'object') {
      res.push(...Object.keys(val).filter(key => key && val[key] && !res.includes(key)))
    } else if (val !== '' && !res.includes((val = String(val)))) {
      res.push(val)
    }
  }
  return res
}

/**
 *  计算样式合集
 *  @param  {String|Array|Object} value 样式源 ( String: CssText | Array[String] | Object: { CssKey: CssValue } )
 *  @return {Object}                    处理后的样式合集
 */
export function computeStyle(value) {
  const res = {}
  for (let val of [value].flat(Infinity)) {
    if (val == null) continue
    if (typeof val == 'object') {
      for (let key in val) res[key] = val[key]
    } else if (typeof val == 'string' && val !== '') {
      for (let item of val.split(';')) {
        item = item.split(':')
        if (item.length == 2 && (item[0] = item[0].trim())) {
          res[item[0]] = item[1].trim()
        }
      }
    }
  }
  return res
}

/**
 *  计算自适应宽高大小
 *  @param  {Object}  options                 配置项
 *  @param  {Number}  options.sourceWidth     原始宽度
 *  @param  {Number}  options.sourceHeight    原始高度
 *  @param  {Number}  [options.targetWidth]   目标宽度 (targetHeight 未同时设置时，目标高度以相同宽高比输出)
 *  @param  {Number}  [options.targetHeight]  目标高度 (targetWidth 未同时设置时，目标宽度以相同宽高比输出)
 *  @param  {Number}  [options.minSide]       短边最小长度 (以相同宽高比缩放，优先级高于 maxSide, targetWidth, targetHeight)
 *  @param  {Number}  [options.maxSide]       长边最大长度 (以相同宽高比缩放，优先级低于 minSide, targetWidth, targetHeight)
 *  @param  {Boolean} [options.round]         是否四舍五入取整 ( true: 默认值，是 | false: 否 )
 *  @return {Object[Number]}                  计算后的宽高 { width, height }
 */
export function computeFitSize(options) {
  let { sourceWidth: width, sourceHeight: height, targetWidth, targetHeight, minSide, maxSide, round = true } = options || {}
  if (targetWidth || targetHeight) {
    let sourceWidth = width, sourceHeight = height
    width = targetWidth || targetHeight * (sourceWidth / sourceHeight)
    height = targetHeight || targetWidth * (sourceHeight / sourceWidth)
  }
  if (minSide || maxSide) {
    let isWide = width > height, sourceWidth = width, sourceHeight = height
    if (minSide && minSide > (isWide ? height : width)) {
      width = isWide ? minSide * (sourceWidth / sourceHeight) : minSide
      height = isWide ? minSide : minSide * (sourceHeight / sourceWidth)
    } else if (maxSide && maxSide < (isWide ? width : height)) {
      width = isWide ? maxSide : maxSide * (sourceWidth / sourceHeight)
      height = isWide ? maxSide * (sourceHeight / sourceWidth) : maxSide
    }
  }
  if (round) width = Math.round(width), height = Math.round(height)
  return { width, height }
}

/**
 *  压缩图片
 *  @param  {String|File} file                    原始图片 ( String: 图片路径 | File: 文件流 )
 *  @param  {Object}      [options]               配置项
 *  @param  {Number}      [options.targetWidth]   目标宽度 (targetHeight 未同时设置时，目标高度以相同宽高比输出)
 *  @param  {Number}      [options.targetHeight]  目标高度 (targetWidth 未同时设置时，目标宽度以相同宽高比输出)
 *  @param  {Number}      [options.minSide]       短边最小长度 (以相同宽高比缩放，优先级高于 maxSide, targetWidth, targetHeight)
 *  @param  {Number}      [options.maxSide]       长边最大长度 (以相同宽高比缩放，优先级低于 minSide, targetWidth, targetHeight)
 *  @param  {Number}      [options.quality]       图片压缩质量 (默认值: 0.9，值范围: 0 - 1，仅图片类型为支持压缩的类型 'jpeg' 或 'webp' 生效)
 *  @param  {String}      [options.imageType]     输出图片类型 ( 'jpg': 默认值 | 'png' | 'webp' )
 *  @param  {String}      [options.outputType]    输出类型 ( 'file': 默认值 | 'blob' | 'base64' )
 *  @return {Promise}
 */
export async function compressImage(file, options) {
  let fileType = file instanceof Blob ? 1 : typeof file == 'string' && file ? 2 : 0
  if (fileType == 0) throw new TypeError('<file> must be a Blob / File / String')
  let image = new Image()
  await new Promise((resolve, reject) => {
    image.onload = resolve
    image.onerror = reject
    if (fileType == 2) {
      image.crossOrigin = 'anonymous'
      image.src = file
    } else {
      let reader = new FileReader()
      reader.onload = () => image.src = reader.result
      reader.onerror = reject
      reader.readAsDataURL(file)
    }
  })
  return await new Promise((resolve, reject) => {
    let { naturalWidth: sourceWidth, naturalHeight: sourceHeight } = image
    let { targetWidth, targetHeight, minSide, maxSide, quality = 0.9, imageType = 'jpg', outputType = 'file' } = options || {}
    let { width, height } = computeFitSize({ sourceWidth, sourceHeight, targetWidth, targetHeight, minSide, maxSide })
    let mimeType = `image/${imageType && imageType != 'jpg' ? imageType : 'jpeg'}`
    let canvas = document.createElement('canvas')
    let context = canvas.getContext('2d')
    canvas.width = width
    canvas.height = height
    context.drawImage(image, 0, 0, width, height)
    canvas.toBlob(blob => {
      if (!blob) {
        reject(new Error('Blob export failed'))
      } else if (outputType == 'base64') {
        let reader = new FileReader()
        reader.onload = () => resolve(reader.result)
        reader.onerror = reject
        reader.readAsDataURL(blob)
      } else if (outputType == 'blob') {
        blob[fileType == 1 ? 'raw' : 'rawPath'] = file
        resolve(blob)
      } else {
        let name = fileType == 1 && ((/(.*)\..+$/.exec(file.name || '') || [])[1] || file.name) || new Date().valueOf()
        let resFile = new File([blob], `${name}.${imageType}`, { type: mimeType })
        resFile[fileType == 1 ? 'raw' : 'rawPath'] = file
        resolve(resFile)
      }
    }, mimeType, quality)
  })
}

/**
 *  压缩图片 (小程序)
 *  @param  {String}  filePath                图片路径
 *  @param  {Object}  [options]               配置项
 *  @param  {Number}  [options.targetWidth]   目标宽度 (targetHeight 未同时设置时，目标高度以相同宽高比输出) [仅微信小程序]
 *  @param  {Number}  [options.targetHeight]  目标高度 (targetWidth 未同时设置时，目标宽度以相同宽高比输出) [仅微信小程序]
 *  @param  {Number}  [options.minSide]       短边最小长度 (以相同宽高比缩放，优先级高于 maxSide, targetWidth, targetHeight) [仅微信小程序]
 *  @param  {Number}  [options.maxSide]       长边最大长度 (以相同宽高比缩放，优先级低于 minSide, targetWidth, targetHeight) [仅微信小程序]
 *  @param  {Number}  [options.quality]       图片压缩质量 (默认值: 0.9，值范围: 0 - 1，仅图片格式为支持压缩的类型 'image/jpeg' 生效)
 *  @param  {String}  [options.outputType]    输出类型 ( 'path': 默认值，文件路径 | 'buffer' | 'binary' | 'base64' | 'ascii' | 'hex' | 'latin1' | 'utf8'/'utf-8' | 'ucs2'/'ucs-2' | 'utf16le'/'utf-16le' )
 *  @return {Promise}
 */
export async function compressImageMP(filePath, options) {
  let { quality = 0.9, outputType = 'path' } = options || {}
  // #ifdef MP-WEIXIN
  let { width: sourceWidth, height: sourceHeight } = await new Promise((success, fail) => (
    uni.getImageInfo({ src: filePath, success, fail })
  ))
  let { targetWidth, targetHeight, minSide, maxSide } = options || {}
  let { width, height } = computeFitSize({ sourceWidth, sourceHeight, targetWidth, targetHeight, minSide, maxSide })
  // #endif
  let { tempFilePath } = await new Promise((success, fail) => (
    uni.compressImage({
      src: filePath, quality: quality * 100, success, fail,
      // #ifdef MP-WEIXIN
      compressedWidth: width, compressedHeight: height
      // #endif
    })
  ))
  if (outputType && outputType != 'path') {
    let { data: outputData } = await new Promise((success, fail) => (
      uni.getFileSystemManager().readFile({
        filePath: tempFilePath, success, fail,
        ...outputType != 'buffer' ? { encoding: outputType } : {}
      })
    ))
    if (outputType == 'base64') {
      let { type } = await new Promise((success, fail) => (
        uni.getImageInfo({ src: tempFilePath, success, fail })
      ))
      return `data:image/${type && type != 'jpg' ? type : 'jpeg'};base64,${outputData}`
    }
    return outputData
  }
  return tempFilePath
}
